﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ApmetrixDLL
{
    public static class Apmetrix
    {

        /**
     * Return status definition: All okay
     */
        public static readonly int SUCCESS = 0;
        /**
         * Return status definition: Warning
         */
        public static readonly int WARNING = 100;
        /**
         * Return status definition: Warning, function call ignored, init() or restartSession() already called
         */
        public static readonly int WARNING_IGNORED_SESSION_ALREADY_OPEN = WARNING + 1;
        /**
         * Return status definition: Warning, function call ignored, init() not yet called
         */
        public static readonly int WARNING_IGNORED_SESSION_NOT_INITIALIZED = WARNING + 2;
        /**
         * Return status definition: Warning, function call ignored, init() or restartSession() not yet called
         */
        public static readonly int WARNING_IGNORED_SESSION_NOT_OPEN = WARNING + 3;
        /**
         * Return status definition: Warning, some event parameters were ignored
         */
        public static readonly int WARNING_IGNORED_SOME_EVENT_PARAMETERS = WARNING + 4;
        /**
         * Return status definition: Warning, push notifications could not be enabled
         */
        public static readonly int WARNING_COULD_NOT_ENABLE_PUSH_NOTIFICATIONS = WARNING + 5;
        /**
         * Return status definition: Error
         */
        public static readonly int ERROR = 200;
        /**
         * Return status definition: Error, function called with invalid arguments
         */
        public static readonly int ERROR_INVALID_ARGUMENTS = ERROR + 1;
        /**
         * Return status definition: Error, application configuration error
         */
        public static readonly int ERROR_APP_CONFIG = ERROR + 2;
        /**
         * Return status definition: Error, event queue full - event dropped error
         */
        public static readonly int ERROR_EVENT_DROPPED = ERROR + 3;
        /**
         * Return status definition: Error, function call failed
         */
        public static readonly int ERROR_FAILED = ERROR + 4;

        // poll timeouts used when buffering data from the queue
        private static readonly int SHORT_POLL_TIMEOUT_MIN = 15000;		// 15 secs (in msec)

        private static readonly bool IS_DEBUG = true;
        /**
         * Class for pre-defined Custom Event parameters
         */
        public enum CustomEvent
        {
            /**
             * Table value for un-grouped custom events 
             */
            UN_GROUPED_TABLE,
            /**
             * Table value for user grouped custom events 
             */
            USERS_TABLE,
            /**
             * Table value for application grouped custom events 
             */
            APPS_TABLE,
            /**
             * Table value for location grouped custom events 
             */
            TRACK_TABLE
        }


        static string valueOf(this CustomEvent CE)
        {
            int table = 1;
            switch (CE)
            {
                case CustomEvent.UN_GROUPED_TABLE:
                    table = 1;
                    break;
                case CustomEvent.USERS_TABLE:
                    table = 2;
                    break;
                case CustomEvent.APPS_TABLE:
                    table = 3;
                    break;
                case CustomEvent.TRACK_TABLE:
                    table = 4;
                    break;
            }
            return table.ToString();
        }

        /**
        * Class for pre-defined Permission values
        */
        public enum Permission
        {
            ALLOW_TRACKING,
            BLOCK_TRACKING,
            CHILD_SAFE
        }

        private static readonly int MAX_CUSTOM_EVENT_DIMENSIONS = 25;
        private static readonly int MAX_CUSTOM_EVENT_METRICS = 5;

        private static readonly int MIN_AP_TRAK_AXES = 2;
        private static readonly int MAX_AP_TRAK_AXES = 4;

        private static ApmetrixSession session = null;
        //   // private static ApmetrixTimerEvent ate = null;
        private static ApmetrixThread thread = null;


        public static int getThreadSession()
        {
            if (IS_DEBUG)
                return ApmetrixThread.getSessionID();
            else
                return 0;
        }

        public static int getThreadPack()
        {
            if (IS_DEBUG)
                return ApmetrixThread.getPackets();
            else
                return 0;
        }

        public static int getThreadSessionCount()
        {
            if (IS_DEBUG)
                return ApmetrixThread.getSesCount();
            else
                return 0;
        }

        public static string getIP()
        {
            if (IS_DEBUG)
                return ApmetrixThread.getIPAd();
            else
                return "N/A";
        }

        public static string getUDID()
        {
            if (IS_DEBUG)
                return ApmetrixThread.UDID();
            else
                return "N/A";
        }

        private static readonly string TAG = "Apmetrix SDK";

        private static int currentGameDataset = 0;
        private static string currentGameVersion = null;
        private static string currentGameUnifier = null;
        private static Permission currentPermission = Permission.ALLOW_TRACKING;

        private static bool ignoredEventParameters;

        //   /**
        //    * Library method to initialize (start) session 
        //    * but with GCM push notification functionality disabled
        //    * 
        //    * @param appDataset dataset Id for the application
        //    * @param appVersion version of the application
        //    * @param appUnifier unifier for the application
        //    * @return {@link Apmetrix#SUCCESS} <br>
        //    * {@link Apmetrix#WARNING_IGNORED_SESSION_ALREADY_OPEN} <br>
        //    * {@link Apmetrix#ERROR_INVALID_ARGUMENTS} <br>
        //    * {@link Apmetrix#ERROR_APP_CONFIG} <br>
        //    * {@link Apmetrix#ERROR_EVENT_DROPPED} <br>
        //    * {@link Apmetrix#ERROR_FAILED}
        //    */
        public static int init(int appDataset, string appVersion, string appUnifier)
        {
            Console.WriteLine("INIT APMETRIX");
            //ApmetrixDBInterface adb = new ApmetrixDBInterface("TestDB");
            //Console.WriteLine("DATABASE TEST STATUS: " + adb.open());
            //adb.insertUrls("jbsafjbh", 20144);
            //adb.insertUrls("IAMkkkkkklllAURL", 26789012);
            //adb.deleteFromUrls(1);
            //adb.insertOrReplaceSessions(1, 0, 1374606328426);
            //adb.updateSessions(12377, 1234);
            //adb.selectNextSessionData();
            //Console.WriteLine(adb.selectCountFromSessions());
            //Console.WriteLine(adb.selectCountFromUrls());
            //Console.WriteLine(adb.selectCountFromSessionsAndUrls());
            //adb.selectUrl();

            //adb.insertOrReplaceTimers("TestTag3", 123377);
            //adb.selectHighestSessionFromJoin(); //Need to figure out. Not working

            //adb.insertOrReplaceSessions(20, 120513, 20548);
            //return 0;
            return initSDK(appDataset, appVersion, appUnifier);
        }


        /**
     * Library method to initialize (start) session
     * and enable GCM push notification functionality
     * 
     * @param activity calling activity
     * @param appDataset dataset Id for the application
     * @param appVersion version of the application
     * @param appUnifier unifier for the application
     * @param gcmSenderId sender Id (GCM)
     * @return {@link Apmetrix#SUCCESS} <br>
     * {@link Apmetrix#WARNING_IGNORED_SESSION_ALREADY_OPEN} <br>
     * {@link Apmetrix#WARNING_COULD_NOT_ENABLE_PUSH_NOTIFICATIONS} <br>
     * {@link Apmetrix#ERROR_INVALID_ARGUMENTS} <br>
     * {@link Apmetrix#ERROR_APP_CONFIG} <br>
     * {@link Apmetrix#ERROR_EVENT_DROPPED} <br>
     * {@link Apmetrix#ERROR_FAILED}
     */
        public static int initSDK(int appDataset, string appVersion, string appUnifier)
        {
            //Test Build Not implemented Yet
            //if (TEST_BUILD)
            //{
            //    if (testAutoTest && testGameDataset != 0)
            //        appDataset = testGameDataset;
            //    if (testAutoTest && testGameUnifier != null)
            //        appUnifier = testGameUnifier;
            //}

            int status = SUCCESS;

            try
            {
                // if session open
                if (session != null)
                {
                    throw new ApmetrixException(TAG, WARNING_IGNORED_SESSION_ALREADY_OPEN,
                            "Call to init() ignored, session already opened");
                }
                // check arguments
                else if (appDataset == 0)
                {
                    throw new ApmetrixException(TAG, ERROR_INVALID_ARGUMENTS,
                            "Calls to init() must include valid application dataset");
                }
                else if (appUnifier == null)
                {
                    throw new ApmetrixException(TAG, ERROR_INVALID_ARGUMENTS,
                            "Calls to init() must include valid application unifier");
                }

                //           // get handle for the background thread
                thread = ApmetrixThread.getInstance(appDataset);
                //           // create a session
                session = new ApmetrixSession(appDataset, appVersion, appUnifier);

                // create event
                ApmetrixEvent metrixEvent = new ApmetrixStartSessionEvent(session);

                Console.WriteLine("{0} {1} {2} ", session.datasetId, session.gameVersion, session.gameUnifier);
                Console.WriteLine("{0} {1} {2} ", metrixEvent.getGamesetId(), metrixEvent.getStartSessionTime(), metrixEvent.getGameUnifier());
                Console.WriteLine("{0}", ApmetrixServerUtilities.getExtIPAddress());
                ////offer event to the queue
                if (thread.queue.TryAdd(metrixEvent, SHORT_POLL_TIMEOUT_MIN))
                {
                    // save parameters for restart
                    currentGameDataset = appDataset;
                    currentGameVersion = appVersion;
                    currentGameUnifier = appUnifier;
                }
                else
                {
                    // log error
                    //new ApmetrixException.ApmetrixException(TAG, "Queue full, event dropped");
                    Console.WriteLine("{0} EXCEPTION: Queue full, event dropped", TAG);
                    session = null;
                    status = ERROR_EVENT_DROPPED;
                }

                Console.WriteLine("Thread Queue Count: {0}", thread.queue.Count);
            }
            catch (ApmetrixException ae)
            {
                // get exception status
                status = ae.getExceptionStatus();
                if (status > ERROR)
                {
                    // clear session
                    session = null;
                }
            }

            return status;
        }

        /**
         * Default library method to restart session
         * @return {@link Apmetrix#SUCCESS} <br>
         * {@link Apmetrix#WARN_IGNORED_SESSION_NOT_INITIALIZED} <br>
         * {@link Apmetrix#ERR_INVALID_ARGUMENTS} <br>
         * {@link Apmetrix#ERROR_EVENT_DROPPED} <br>
         * {@link Apmetrix#ERR_FAILED}
         */
        public static int resetartSession()
        {
            int status = SUCCESS;

            try
            {
                if (!ApmetrixThread.validGameDataset)
                    throw new ApmetrixException(TAG, ERROR_INVALID_ARGUMENTS,
                        "Game dataset passed during init() was invalid");
                else if (!ApmetrixThread.validGameUnifier)
                    throw new ApmetrixException(TAG, ERROR_INVALID_ARGUMENTS,
                            "Game unifier passed during init() was invalid");
                //if never initialized session
                else if (currentGameDataset == 0 || currentGameVersion == null || currentGameUnifier == null)
                    throw new ApmetrixException(TAG, WARNING_IGNORED_SESSION_NOT_INITIALIZED,
                        "Call to restartSession() ignored, can't restart session that hasn't been initialized");
                else if (thread == null)
                {
                    throw new ApmetrixException(TAG, ERROR_FAILED,
                            "Lost reference to background thread");
                }
                status = init(currentGameDataset, currentGameVersion, currentGameUnifier);

            }

            catch (ApmetrixException ae)
            {
                // clear session
                session = null;
                status = ae.getExceptionStatus();
            }

            return status;
        }

        /**
        * Library method to end session
        * @return {@link Apmetrix#SUCCESS} <br>
        * {@link Apmetrix#WARNING_IGNORED_SESSION_NOT_OPEN} <br>
        * {@link Apmetrix#ERR_INVALID_ARGUMENTS} <br>
        * {@link Apmetrix#ERROR_EVENT_DROPPED} <br>
        * {@link Apmetrix#ERROR_FAILED}
        */
        public static int endSession()
        {
            int status = SUCCESS;

            try
            {
                if (!ApmetrixThread.validGameDataset)
                    throw new ApmetrixException(TAG, ERROR_INVALID_ARGUMENTS,
                        "Game dataset passed during init() was invalid");

                else if (!ApmetrixThread.validGameUnifier)
                    throw new ApmetrixException(TAG, ERROR_INVALID_ARGUMENTS,
                        "Game unifier passed during init() was invalid");

                //if session not started
                else if (session == null)
                    throw new ApmetrixException(TAG, WARNING_IGNORED_SESSION_NOT_OPEN,
                        "Call to endSession() ignored, no session currently open");

                else if (thread == null)
                    throw new ApmetrixException(TAG, ERROR_FAILED,
                        "Lost reference to background thread");

                //create event data
                ApmetrixEvent metrixEvent = new ApmetrixEndSessionEvent(session);

                //offer event to the queue
                if (thread.queue.TryAdd(metrixEvent, SHORT_POLL_TIMEOUT_MIN))
                    //mark end of session
                    session = null;
                else
                {
                    //log error
                    new ApmetrixException(TAG, ERROR_EVENT_DROPPED, "Queue Full, event dropped");
                    status = ERROR_EVENT_DROPPED;
                }

                metrixEvent = null;
            }
            catch (ApmetrixException ae)
            {
                //get exception status
                status = ae.getExceptionStatus();
            }


            return status;
        }


        public static int setTracking(Permission trackingPermissions)
        {
            currentPermission = trackingPermissions;
            return SUCCESS;
        }

        /**
         * Library method to log repeated custom events
         * @param table pre-defined custom event category
         * @param dimensions Variable sized list (up to 25 max) of dimensions for the custom event.
         * This list can be any dimensions you'd like to track but for ease of understanding when
         * generating meaningful reports it is suggested the first 4 be ordered as: verb, object, 
         * location and value<br>
         * If ordering event dimensions in this manner, use null for dimensions that have no value<br>
         * and prepend a "#" before any numerical value that you need to do math functions on<br>
         *    <br>
         *    An example of a function call for a user event of a racing game application might be:<br>
         *    &nbsp&nbsp logEvent(Apmetrix.CustomEvent.USERS_TABLE, "Crashed", "Time Trial", "Le Mans", "2 mins 34 secs");<br>
         *    <br>
         *    An example of a function call for an application event might be:<br>
         *    &nbsp&nbsp logEvent(Apmetrix.CustomEvent.APPS_TABLE, "Logon", null, "Facebook"};<br> 
         *    <br>
         *    An example of a function call for an un-grouped event might be:<br>
         *    &nbsp&nbsp logEvent(Apmetrix.CustomEvent.UN_GROUPED_TABLE, "Start Session", "Virtual Currency", "#2500"};<br> 
         * 	  <br>
         * @return {@link Apmetrix#SUCCESS} <br>
         * {@link Apmetrix#WARNING_IGNORED_SESSION_NOT_OPEN} <br>
         * {@link Apmetrix#WARNING_IGNORED_SOME_EVENT_PARAMETERS} <br>
         * {@link Apmetrix#ERROR_INVALID_ARGUMENTS} <br>
         * {@link Apmetrix#ERROR_EVENT_DROPPED} <br>
         * {@link Apmetrix#ERROR_FAILED} <br>
         * @see 
         * {@link Apmetrix.CustomEvent#APPS_TABLE} <br>
         * {@link Apmetrix.CustomEvent#UN_GROUPED_TABLE} <br>
         * {@link Apmetrix.CustomEvent#USERS_TABLE}
         */
        public static int logEvent(CustomEvent table, String[] dimensions)
        {
            return logEvent(table, null, dimensions);
        }


        /**
         * Library method to log one time custom events
         * @param table Pre-defined custom event group (i.e. user's or application's related or un-grouped)
         * @param tag String to define one time event
         * @param dimensions Variable sized list (up to 25 max) of dimensions for the custom event.
         * This list can be any dimensions you'd like to track but for ease of understanding when
         * generating meaningful reports it is suggested the first 4 be ordered as: verb, object, 
         * location and value<br>
         * If ordering event dimensions in this manner, use null for dimensions that have no value<br>
         * and prepend a "#" before any numerical value that you need to do math functions on<br>
         *    <br>
         *    An example of a function call for an un-grouped event might be:<br>
         *    &nbsp&nbsp logOneTimeEvent(Apmetrix.CustomEvent.UN_GROUPED_TABLE, "Start Session", "Virtual Currency", "#2500"};<br> 
         * 	  <br>
         * @return {@link Apmetrix#SUCCESS} <br>
         * {@link Apmetrix#WARNING_IGNORED_SESSION_NOT_OPEN} <br>
         * {@link Apmetrix#WARNING_IGNORED_SOME_EVENT_PARAMETERS} <br>
         * {@link Apmetrix#ERROR_INVALID_ARGUMENTS} <br>
         * {@link Apmetrix#ERROR_EVENT_DROPPED} <br>
         * {@link Apmetrix#ERROR_FAILED} <br>
         * @see 
         * {@link Apmetrix.CustomEvent#APPS_TABLE} <br>
         * {@link Apmetrix.CustomEvent#UN_GROUPED_TABLE} <br>
         * {@link Apmetrix.CustomEvent#USERS_TABLE}
         */
        public static int logOneTimeEvent(CustomEvent table, string oneTimeTag, string[] dimensions)
        {
            return logEvent(table, oneTimeTag, dimensions);
        }


        /**
         * Library method to log application tracking event locations
         * @param event String denoting the event or action being tracked
         * @param location String denoting the location or screen within the application the event occurred
         * @param coordinates Variable sized list of coordinates for the tracked event (min 2 - max 4).
         * <br><br>
         * Note: the coordinates need to be ordered as follows:<br>
         * &nbsp&nbsp X, Y, Z (optional) and W (optional) 
         * 	  <br>
         *    An example of a function call to log a tracking event at x=644.25, y=127.25 and z=25.5 might be:<br>
         *    &nbsp&nbsp Apmetrix.ApTrak("touched", "screen4", "644.25", "127.75", "25.50");<br> 
         * 	  <br>
         * @return {@link Apmetrix#SUCCESS} <br>
         * {@link Apmetrix#WARNING_IGNORED_SESSION_NOT_OPEN} <br>
         * {@link Apmetrix#WARNING_IGNORED_SOME_EVENT_PARAMETERS} <br>
         * {@link Apmetrix#ERROR_INVALID_ARGUMENTS} <br>
         * {@link Apmetrix#ERROR_EVENT_DROPPED} <br>
         * {@link Apmetrix#ERROR_FAILED}
         */
        public static int ApTrak(string metrixEvent, string location, string[] coordinates)
        {
            int numAxes = 0;
            int status = SUCCESS;
            List<string> parameters = new List<string>();
            parameters.Add(metrixEvent);
            parameters.Add(location);

            foreach (string coord in coordinates)
            {
                parameters.Add(coord);
                numAxes++;
            }

            try
            {
                if (numAxes < MIN_AP_TRAK_AXES)
                    throw new ApmetrixException(TAG, ERROR_INVALID_ARGUMENTS,
                        "At least " + MIN_AP_TRAK_AXES + " coordinates are required to define an event location");
                else if (numAxes > MAX_AP_TRAK_AXES)
                    throw new ApmetrixException(TAG, WARNING_IGNORED_SOME_EVENT_PARAMETERS,
                            "No more than " + MAX_AP_TRAK_AXES + " coordinates can be used to define an event location");
                else
                    status = logEvent(CustomEvent.TRACK_TABLE, parameters.ToArray());
            }
            catch (ApmetrixException ae)
            {
                // get exception status
                status = ae.getExceptionStatus();
            }

            return status;
        }

        /*******************************************************************************
                            Private methods
         ******************************************************************************/

        private static int logEvent(CustomEvent table, string tag, string[] dimensions)
        {
            ignoredEventParameters = false;
            Dictionary<string, string> eventData = buildCustomEventData(table, tag, dimensions);
            return logEvent(eventData);
        }


        private static int logEvent(Dictionary<String, String> eventData)
        {
            int status = SUCCESS;
            try
            {
                // if error with gameDataset parameter for init()
                if (!ApmetrixThread.validGameDataset)
                {
                    throw new ApmetrixException(TAG, ERROR_INVALID_ARGUMENTS,
                            "Game dataset passed during init() was invalid");
                }
                // else if error with gameUnifier parameter for init()
                else if (!ApmetrixThread.validGameUnifier)
                {
                    throw new ApmetrixException(TAG, ERROR_INVALID_ARGUMENTS,
                            "Game unifier passed during init() was invalid");
                }
                // else if session not started
                else if (session == null)
                {
                    throw new ApmetrixException(TAG, WARNING_IGNORED_SESSION_NOT_OPEN,
                            "Call to logEvent() ignored, session not open");
                }
                // else if lost reference to thread object
                else if (thread == null)
                {
                    throw new ApmetrixException(TAG, ERROR_FAILED,
                            "Lost reference to background thread");
                }
                // else if some of the parameters to this logEvent() call were ignored
                else if (ignoredEventParameters)
                {
                    throw new ApmetrixException(TAG, WARNING_IGNORED_SOME_EVENT_PARAMETERS,
                            "Exceeded number of parameters that can be handled when logging event data");
                }
                // create event
                ApmetrixEvent metrixEvent = new ApmetrixApplicationEvent(session, eventData);

                // offer event to the queue
                if (!thread.queue.TryAdd(metrixEvent, SHORT_POLL_TIMEOUT_MIN))
                {
                    throw new ApmetrixException(TAG, ERROR_EVENT_DROPPED, "Queue full, event dropped");
                }
            }
            catch (ApmetrixException ae)
            {
                // clear session
                session = null;
                // get exception status
                status = ae.getExceptionStatus();
            }
            return status;
        }


        /*
         * Build key/value event data for custom event paramters
         */

        private static Dictionary<string, string> buildCustomEventData(CustomEvent table,
                string oneTimeTag, string[] parameters)
        {
            int dimensionCount = 0, metricCount = 0;
            string key, value, dimensionDebugMessage = null, metricsDebugMessage = null;
            Dictionary<string, string> eventData = new Dictionary<string, string>();
            int numParameters = parameters.Length;

            if (!eventData.ContainsKey(ApmetrixConstants.Key.CUSTOM_EVENT_TABLE))
                eventData.Add(ApmetrixConstants.Key.CUSTOM_EVENT_TABLE, table.valueOf());

            if (!eventData.ContainsKey(ApmetrixConstants.Key.CUSTOM_EVENT_METRIC))
                eventData.Add(ApmetrixConstants.Key.CUSTOM_EVENT_METRIC + ++metricCount, "1");

            if (oneTimeTag != null)
            {
                if (!eventData.ContainsKey(ApmetrixConstants.Key.CUSTOM_EVENT_KEY))
                    eventData.Add(ApmetrixConstants.Key.CUSTOM_EVENT_KEY, oneTimeTag);
            }
            for (int i = 0; i < numParameters; i++)
            {
                //if a valid parameter
                if ((value = parameters[i]) != null)
                {
                    // if this parameter is a metric
                    if (value.StartsWith("#", 0))
                    {
                        if (metricCount < MAX_CUSTOM_EVENT_METRICS)
                        {
                            key = ApmetrixConstants.Key.CUSTOM_EVENT_METRIC + ++metricCount;
                            //strip metric indicator
                            if (!eventData.ContainsKey(key))
                                eventData.Add(key, value.Substring(1));
                        }
                        else if (metricsDebugMessage == null)
                        {
                            //only log the first occurrence
                            metricsDebugMessage =
                                "Only accepting the first " +
                                (MAX_CUSTOM_EVENT_METRICS - 1) +
                                " metrics ";
                            Console.WriteLine("{0} WARNING: {1}", TAG, metricsDebugMessage);
                            ignoredEventParameters = true;
                        }
                    }
                    else
                    {
                        if (dimensionCount < MAX_CUSTOM_EVENT_DIMENSIONS)
                        {
                            key = ApmetrixConstants.Key.CUSTOM_EVENT_DIMENSION + ++dimensionCount;
                            if (!eventData.ContainsKey(key))
                                eventData.Add(key, value);
                        }
                        else if (dimensionDebugMessage == null)
                        {
                            //only log the first occurrence
                            dimensionDebugMessage =
                                "Only accepting the first " +
                                MAX_CUSTOM_EVENT_DIMENSIONS +
                                " dimensions ";
                            Console.WriteLine("{0} WARNING: {1}", TAG, metricsDebugMessage);
                            ignoredEventParameters = true;
                        }
                    }
                }
            }

            return eventData;

        }


    }
}
